/* Copyright (C) 2009, 2011 Christofer Jonsson
 *
 * This file is part of FXH.
 *
 * FXH is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * FXH is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with FXH.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "fxh_net.h"
#include "fxh_util.h"
#include "fxh_error.h"
#include "fxh_pbar.h"
#include <netinet/in.h>
#include <endian.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

/* Protocol version */
static const uint32_t proto_version = 1;

#define RECV_BUFFER_SIZE (1048576)
#define SEND_BUFFER_SIZE (1048576)

typedef struct
{
   uint32_t version;
   uint32_t nfiles;
} basic_info_t;

typedef struct
{
   uint32_t name_length;
   uint64_t file_size;
} file_info_t;

static fxh_status_t send_file(fxh_socket_t *peer, char *path);
static fxh_status_t recv_file(fxh_socket_t *peer);
static fxh_status_t send_file_data(fxh_socket_t *peer, const char *path, uint64_t size);
static fxh_status_t recv_file_data(fxh_socket_t *peer, const char *filename, uint64_t size);
static fxh_status_t send_basic_info(fxh_socket_t *peer, const basic_info_t *info);
static fxh_status_t recv_basic_info(fxh_socket_t *peer, basic_info_t *info);
static fxh_status_t send_file_info(fxh_socket_t *peer, const file_info_t *info);
static fxh_status_t recv_file_info(fxh_socket_t *peer, file_info_t *info);

fxh_status_t fxh_net_send_files(fxh_socket_t *peer, int nfiles, char **files)
{
   basic_info_t basic_info = { 0, 0 };
   int i = 0;
   basic_info.version = proto_version;
   basic_info.nfiles = (uint32_t)nfiles;

   if (send_basic_info(peer, &basic_info) == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   for (i = 0; i < nfiles; ++i)
   {
      if (send_file(peer, files[i]) == FXH_FAILURE)
      {
	 return FXH_FAILURE;
      }
   }
   
   return FXH_SUCCESS;
}

fxh_status_t fxh_net_recv_files(fxh_socket_t *peer)
{
   basic_info_t basic_info = { 0, 0 };
   uint32_t i = 0;

   if (recv_basic_info(peer, &basic_info) == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   if (basic_info.version != proto_version)
   {
      FXH_ERROR("Protocol version mismatch");
      return FXH_FAILURE;
   }
   
   for (i = 0; i < basic_info.nfiles; ++i)
   {
      if (recv_file(peer) == FXH_FAILURE)
      {
	 return FXH_FAILURE;
      }
   }

   return FXH_SUCCESS;
}

static fxh_status_t send_file(fxh_socket_t *peer, char *path)
{
   file_info_t file_info = { 0, 0 };
   size_t offset = 0;
   size_t path_length = strlen(path);

   if (fxh_util_file_size(path, &file_info.file_size) == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   if (fxh_util_basename(path, path_length, &offset) == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   file_info.name_length = path_length - offset;

   if (send_file_info(peer, &file_info) == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   if (fxh_socket_send_all(peer, path + offset, file_info.name_length) == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   if (send_file_data(peer, path, file_info.file_size) == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   return FXH_SUCCESS;
}

static fxh_status_t recv_file(fxh_socket_t *peer)
{
   file_info_t file_info = { 0, 0 };
   char *filename = NULL;

   if (recv_file_info(peer, &file_info) == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   filename = (char*)fxh_util_malloc(file_info.name_length + 1);

   if (fxh_socket_recv_all(peer, filename, file_info.name_length) == FXH_FAILURE)
   {
      goto failed;
   }

   filename[file_info.name_length] = '\0';

   if (recv_file_data(peer, filename, file_info.file_size) == FXH_FAILURE)
   {
      goto failed;
   }
   
   fxh_util_free(filename);
   return FXH_SUCCESS;

  failed:

   fxh_util_free(filename);
   return FXH_FAILURE;
}

static fxh_status_t send_file_data(fxh_socket_t *peer, 
				   const char *path, 
				   uint64_t size)
{
   char buffer[SEND_BUFFER_SIZE];
   size_t bytes = 0, fn_offset = 0;
   uint64_t sent = 0;
   FILE *file = NULL;

   if (fxh_util_basename(path, strlen(path), &fn_offset) == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   fxh_pbar_init(path + fn_offset, size);
   fxh_pbar_update(sent, 0, 0.0);

   if (size == 0)
   {
      fxh_pbar_finish();
      return FXH_SUCCESS;
   }

   if ((file = fopen(path, "rb")) == NULL)
   {
      FXH_ERROR("Unable to open file %s: %s", path, strerror(errno));
      goto failed;
   }

   while (!feof(file))
   {
      bytes = fread(buffer, 1, SEND_BUFFER_SIZE, file);

      if (ferror(file))
      {
	 FXH_ERROR("Error reading file %s: %s", path, strerror(errno));
	 goto failed;
      }

      if (0 == bytes)
      {
	 break;
      }

      if (fxh_socket_send_all(peer, buffer, bytes) == FXH_FAILURE)
      {
	 goto failed;
      }

      sent += bytes;
      fxh_pbar_update(sent, 
		      peer->stats.last_trans.bytes,
		      peer->stats.last_trans.time);
   }

   (void)fclose(file);
   fxh_pbar_finish();
   return FXH_SUCCESS;

  failed:

   (void)fclose(file);
   fxh_pbar_finish();
   return FXH_FAILURE;   
}

static fxh_status_t recv_file_data(fxh_socket_t *peer, 
				   const char *filename, 
				   uint64_t size)
{
   char buffer[RECV_BUFFER_SIZE];
   uint64_t received = 0;
   size_t bytes = 0, written = 0, receive = 0;
   FILE *file = NULL;

   if ((file = fopen(filename, "wb")) == NULL)
   {
      FXH_ERROR("Unable to open file %s: %s", filename, strerror(errno));
      return FXH_FAILURE;
   }

   fxh_pbar_init(filename, size);
   fxh_pbar_update(received, 0, 0.0);

   while (received != size)
   {
      if (size - received < RECV_BUFFER_SIZE)
      {
	 receive = size - received;
      }
      else
      {
	 receive = RECV_BUFFER_SIZE;
      }	 

      if (fxh_socket_recv(peer, buffer, receive, &bytes) == FXH_FAILURE)
      {
	 goto failed;
      }

      written = fwrite(buffer, 1, bytes, file);

      if (written != bytes)
      {
	 goto failed;
      }

      received += bytes;
      fxh_pbar_update(received,
		      peer->stats.last_trans.bytes,
		      peer->stats.last_trans.time);
   }

   (void)fclose(file);
   fxh_pbar_finish();
   return FXH_SUCCESS;

  failed:

   (void)fclose(file);
   fxh_pbar_finish();
   return FXH_FAILURE;
}

static fxh_status_t send_basic_info(fxh_socket_t *peer, const basic_info_t *info)
{  
   uint32_t version = htonl(info->version);
   uint32_t nfiles = htonl(info->nfiles);   
   fxh_status_t result = FXH_FAILURE;
   
   result = fxh_socket_send_all(peer, &version, sizeof(info->version));

   if (result == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   result = fxh_socket_send_all(peer, &nfiles, sizeof(info->nfiles));

   if (result == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   return FXH_SUCCESS;
}

static fxh_status_t recv_basic_info(fxh_socket_t *peer, basic_info_t *info)
{
   fxh_status_t result = FXH_FAILURE;
   
   result = fxh_socket_recv_all(peer, &info->version, sizeof(info->version));

   if (result == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   result = fxh_socket_recv_all(peer, &info->nfiles, sizeof(info->nfiles));

   if (result == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   info->version = ntohl(info->version);
   info->nfiles = ntohl(info->nfiles);
   return FXH_SUCCESS;
}

static fxh_status_t send_file_info(fxh_socket_t *peer, const file_info_t *info)
{
   fxh_status_t result = FXH_FAILURE;
   uint32_t name_length = htonl(info->name_length);
   uint64_t file_size = htobe64(info->file_size);

   result = fxh_socket_send_all(peer, &name_length, sizeof(info->name_length));

   if (result == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   result = fxh_socket_send_all(peer, &file_size, sizeof(info->file_size));

   if (result == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   return FXH_SUCCESS;   
}

static fxh_status_t recv_file_info(fxh_socket_t *peer, file_info_t *info)
{
   fxh_status_t result = FXH_FAILURE;

   result = fxh_socket_recv_all(peer, &info->name_length, sizeof(info->name_length));

   if (result == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   result = fxh_socket_recv_all(peer, &info->file_size, sizeof(info->file_size));

   if (result == FXH_FAILURE)
   {
      return FXH_FAILURE;
   }

   info->name_length = ntohl(info->name_length);
   info->file_size = be64toh(info->file_size);
   return FXH_SUCCESS;
}
